package com.google.code.UtopiaXml;

import java.util.ArrayList;
import org.w3c.dom.NodeList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * Class represents logic in generating classes according to the XML Schema. The
 * Schema is expected to be valid and in the DOM format given in the
 * constructor. Then the class processes every element in the XML - recursively.
 * The results are saved in the class attributes. Main method is
 * generateClasses() which is only one with public access. It returns class
 * Schema - the representation of parsed XML Schema.
 *
 * @author Jiri Mauritz
 */
public class SchemaParser {

    // attributes for manipulation
    private Document doc;
    private XPath xpath;
    // attributes for data collecting
    private List<ClassTemplate> classes;    // every class from this List will be generated
    private List<ComplexType> roots;        // only root complexTypes (for parser)
    // references in XML Schema structure
    private List<String> complexTypeReferences;  // List of all outer complex types
    private Map<String, Indicator> groups;
    private Map<String, List<Attribute>> attributeGroups;
    private Map<String, Type> simpleTypes;

    /**
     * Constructor initialize document, creates new instance for xpath
     * expressions and assign empty values to the Collections.
     *
     * @param doc DOM Object of the XML Schema
     */
    public SchemaParser(Document doc) {
        this.doc = doc;
        xpath = XPathFactory.newInstance().newXPath();
        classes = new ArrayList<>();
        roots = new ArrayList<>();
        complexTypeReferences = new ArrayList<>();
        groups = new TreeMap<>();
        attributeGroups = new TreeMap<>();
        simpleTypes = new TreeMap<>();
    }

    /**
     * Method starts parsing of the XML Schema. <br/> 
     * <b>1.</b> Preprocessing - Every element,
     * that can be referenced, is added to the reference attributes.<br/>
     * <b>2.</b> Inspect outer elements - The outer elements are inspected and 
     * added to the reference attributes. If there is reference to some other 
     * outer element, it is already known thanks to the preprocessing. The order
     * of the inspected elements is important. For example, SimpleType is first
     * because it can't refer to any other element. Every reference to 
     * a ComplexType is trouble-free, because the reference is held in 
     * the String, which is the name of the class. This class exists or will 
     * exist, because every ComplexType is parsed into the class.<br/>
     * <b>3.</b> Inspect elements - The inner elements can use already created 
     * outer elements.
     *
     * @return XML Schema parsed into needed classes
     * @throws XPathExpressionException
     */
    public Schema generateClases() throws XPathExpressionException {
        Node root = (Node) xpath.evaluate("/schema", doc, XPathConstants.NODE);
        // firstly, process all outer elements, that can be dereferenced
        preprocessing();
        inspectRootSimpleTypes(root);
        inspectRootAttributeGroups(root);
        inspectRootGroups(root);
        inspectRootComplexTypes(root);

        // inspect all root elements
        NodeList elements = (NodeList) xpath.evaluate("element",
                root, XPathConstants.NODESET);
        for (int i = 0; i < elements.getLength(); i++) {
            inspectElement(elements.item(i), true);
        }
        return new Schema(classes, roots, "");
    }

    /**
     * Preprocessing goes over root(outer) elements and adds them into 
     * the Lists. It is necessary, because if all the element, that can be 
     * referenced, wasn't in the Lists, some relationships would fail.
     */
    private void preprocessing() throws XPathExpressionException {
        // go over the complexTypes
        NodeList complexTypeNodes = (NodeList) xpath.evaluate("/schema/child::complexType",
                doc, XPathConstants.NODESET);
        for (int i = 0; i < complexTypeNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(complexTypeNodes.item(i), "name");
            if (name != null) {
                complexTypeReferences.add(name);
            }
        }
        // go over the groups
        NodeList groupNodes = (NodeList) xpath.evaluate("/schema/child::group",
                doc, XPathConstants.NODESET);
        for (int i = 0; i < groupNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(groupNodes.item(i), "name");
            if (name != null) {
                groups.put(name, null);
            }
        }
        // go over the attributeGroups
        NodeList attributeGroupNodes = (NodeList) xpath.evaluate("/schema/child::attributeGroup",
                doc, XPathConstants.NODESET);
        for (int i = 0; i < attributeGroupNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(attributeGroupNodes.item(i), "name");
            if (name != null) {
                attributeGroups.put(name, null);
            }
        }
        // go over the simpleTypes
        NodeList simpleTypeNodes = (NodeList) xpath.evaluate("/schema/child::simpleType",
                doc, XPathConstants.NODESET);
        for (int i = 0; i < simpleTypeNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(simpleTypeNodes.item(i), "name");
            if (name != null) {
                simpleTypes.put(name, null);
            }
        }
    }

    /**
     * Inspect elements attributeGroup in the parent node. 
     * If the inspection is successful, attributes are added in 
     * the Map attributeGroups.
     * @param root the parent of the attributeGroup elements
     * @throws XPathExpressionException 
     */
    private void inspectRootAttributeGroups(Node root) throws XPathExpressionException {
        NodeList attributeGroupNodes = (NodeList) xpath.evaluate("attributeGroup",
                root, XPathConstants.NODESET);
        for (int i = 0; i < attributeGroupNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(attributeGroupNodes.item(i), "name");
            if (name != null) {
                List<Attribute> attributes = new ArrayList<>();
                NodeList attributesNode = (NodeList) xpath.evaluate("attribute",
                        attributeGroupNodes.item(i), XPathConstants.NODESET);
                for (int j = 0; j < attributesNode.getLength(); j++) {
                    attributes.add(inspectAttribute(attributesNode.item(j)));
                }
                attributeGroups.put(name, attributes);
            }
        }
    }

    /**
     * Inspect elements simpleType in the parent node.
     * If the inspection is successful, simpleTypes are added in the Map simpleTypes.
     * @param root the parent of the simpleType elements
     * @throws XPathExpressionException 
     */
    private void inspectRootSimpleTypes(Node root) throws XPathExpressionException {
        NodeList simpleTypeNodes = (NodeList) xpath.evaluate("simpleType",
                root, XPathConstants.NODESET);
        for (int i = 0; i < simpleTypeNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(simpleTypeNodes.item(i), "name");
            if (name != null) {
                simpleTypes.put(name, inspectSimpleType(simpleTypeNodes.item(i), name));
            }
        }
    }

    /**
     * Inspect elements group in the parent node. If the inspection is 
     * successful, group is added in the List groups.
     * @param root the parent of the group elements
     * @throws XPathExpressionException 
     */
    private void inspectRootGroups(Node root) throws XPathExpressionException {
        NodeList groupNodes = (NodeList) xpath.evaluate("group",
                root, XPathConstants.NODESET);
        for (int i = 0; i < groupNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(groupNodes.item(i), "name");
            if (name != null) {
                List<Indicator> indicators = inspectIndicators(groupNodes.item(i), name, 0);
                if (indicators.size() > 0) {
                    // the name is already in the map, so it is replaced
                    groups.put(name, indicators.get(0));
                }
            }
        }
    }

    /**
     * Inspect elements complexType in the parent node. 
     * If the inspection is successful, complexType is added in the List classes.
     * @param root the parent of the complexType elements
     * @throws XPathExpressionException 
     */
    private void inspectRootComplexTypes(Node root) throws XPathExpressionException {
        NodeList complexTypeNodes = (NodeList) xpath.evaluate("complexType",
                root, XPathConstants.NODESET);
        for (int i = 0; i < complexTypeNodes.getLength(); i++) {
            String name = getValueByNameOfAttribute(complexTypeNodes.item(i), "name");
            if (name != null) {
                ComplexType complexType = inspectComplexType(complexTypeNodes.item(i), name);
                // the name is already in the map, so it is replaced
                addToClasses(complexType);
            }
        }
    }

    /**
     * Inspect element 'element' in the given node. <br/>
     * If the complexType is child node of the element, it is inspected and 
     * assigned. The reference between element and complexType is held only by 
     * the String (ComplexTypeReference). <br/>
     * Otherwise the simpleType is inspected.<br/>
     * If the element has attribute ref, the reference is saved in type.<br/>
     * If the isRoot argument is true, method creates the element as root element.
     * That means the element is added in the Lists classes and roots.
     * It is added with no respect to the type of the element.
     * 
     * @param elementNode parent node of the element
     * @param isRoot true if the element is in root element 'schema'
     * @return generated element with correct type and name
     * @throws XPathExpressionException 
     */
    private Element inspectElement(Node elementNode, boolean isRoot) throws XPathExpressionException {
        String name = getValueByNameOfAttribute(elementNode, "name");
        Type type;
        if (name != null) {
            Node complexTypeNode = (Node) xpath.evaluate("complexType", elementNode, XPathConstants.NODE);
            if (complexTypeNode != null) {
                // the element is complex - has more complicated structure
                ComplexType complexType = inspectComplexType(complexTypeNode, name);
                type = new ComplexTypeReference(name);
                addToClasses(complexType);
                if (isRoot) {
                    addToRoots(complexType);
                }
                return getMultiOrSingleElement(elementNode, type, name);
            }
            // simple type or reference
            type = recognizeType(elementNode, name, isRoot);
            if ((type instanceof SimpleType) && (isRoot)) {
                ComplexType complexType = new ComplexType(name);
                complexType.setSimpleContent((SimpleType) type);
                addToClasses(complexType);
                addToRoots(complexType);
            }
            return getMultiOrSingleElement(elementNode, type, name);
        } else {
            // inspect ref: reference to another element
            //              only if the attribute name has no occurrence, the attribute ref is allowed
            String ref = getValueByNameOfAttribute(elementNode, "ref");
            if (ref != null) {
                type = new ComplexTypeReference(ref);
                return getMultiOrSingleElement(elementNode, type, ref);
            } else {
                // if there is neither the name nor the ref, the element is not valid
                throw new IllegalStateException("Wrong structure of XML Schema: "
                        + "element has neither the name nor the ref attribute.");
            }
        }
    }

    /**
     * Inspect element complexType in the given node.
     * The complexType may contain one simple or complex Content or one indicator. 
     * It may contain zero or more attributes.
     *
     * @param complexTypeNode node of the complexType element
     * @param name of the complexType (parent element)
     * @throws XPathExpressionException
     */
    private ComplexType inspectComplexType(Node complexTypeNode, String name) throws XPathExpressionException {
        // simpleContent
        Node simpleContent = (Node) xpath.evaluate("simpleContent", complexTypeNode, XPathConstants.NODE);
        if (simpleContent != null) {
            return inspectContent(simpleContent, name);
        }

        // complexContent
        Node complexContent = (Node) xpath.evaluate("complexContent", complexTypeNode, XPathConstants.NODE);
        if (complexContent != null) {
            return inspectContent(complexContent, name);
        }

        ComplexType complexType = new ComplexType(name);
        // if the attribute 'mixed' is present, set SimpleContent to DefaultTypeName (String)
        String mixed = getValueByNameOfAttribute(complexTypeNode, "mixed");
        if (mixed != null) {
            complexType.setSimpleContent(TypeConverter.getDefaultType());
        }

        List<Indicator> indicators = inspectIndicators(complexTypeNode, complexType.getName(), 0);
        if (indicators.size() > 0) {
            if (indicators.get(0) != null) {
                complexType.setIndicator(indicators.get(0));
            }
        }
        // attributes
        NodeList attributes = (NodeList) xpath.evaluate("attribute",
                complexTypeNode, XPathConstants.NODESET);
        for (int i = 0; i < attributes.getLength(); i++) {
            complexType.addAttribute(inspectAttribute(attributes.item(i)));
        }
        // attibute groups
        inspectAttributeGroups(complexTypeNode, complexType);
        return complexType;
    }

    /**
     * Indicators in the given node is added to the List which will be returned.
     * The method looks for every indicator, although there might be only one -
     * in this case (mostly ComplexType) the calling method takes the first
     * indicator by List.get(0).
     *
     * @param parentOfIndicatorNode can be complexType, indicator, group or extension
     * @param name of the parent element
     * @return list of all indicators in the given node
     * @throws XPathExpressionException
     */
    private List<Indicator> inspectIndicators(Node parentOfIndicatorNode,
            String name, int level) throws XPathExpressionException {
        List<Indicator> indicators = new ArrayList<>();
        // sequences
        NodeList sequences = (NodeList) xpath.evaluate("sequence",
                parentOfIndicatorNode, XPathConstants.NODESET);
        for (int i = 0; i < sequences.getLength(); i++) {
            indicators.add(inspectSequence(sequences.item(i), name,
                    createIndicatorName(name, "Sequence", level, i), level));
        }
        // alls
        NodeList alls = (NodeList) xpath.evaluate("all",
                parentOfIndicatorNode, XPathConstants.NODESET);
        for (int i = 0; i < alls.getLength(); i++) {
            indicators.add(inspectAll(alls.item(i),
                    createIndicatorName(name, "All", level, i), level));
        }
        // choices
        NodeList choices = (NodeList) xpath.evaluate("choice",
                parentOfIndicatorNode, XPathConstants.NODESET);
        for (int i = 0; i < choices.getLength(); i++) {
            indicators.add(inspectChoice(choices.item(i), name,
                    createIndicatorName(name, "Choice", level, i), level));
        }
        // groups
        NodeList groupNodes = (NodeList) xpath.evaluate("group",
                parentOfIndicatorNode, XPathConstants.NODESET);
        for (int i = 0; i < groupNodes.getLength(); i++) {
            indicators.add(inspectGroup(groupNodes.item(i)));
        }
        return indicators;
    }

    /**
     * Inspect the element sequence in the given node.
     * All the elements and indicators are inspected. The argument level tells
     * the method, how deep the recursion in indicators is. This is important
     * when creating names of multisequences - the name has to be original.
     * @param sequenceNode node of the element 'sequence'
     * @param originalName the name of the root indicator, in which it is nested
     * @param name actual name in recursion, e.g.RootSubSub1Sequence
     * @param level deep of the recursion in indicators
     * @return inspected indicator sequence
     * @throws XPathExpressionException 
     */
    private Indicator inspectSequence(Node sequenceNode, String originalName,
            String name, int level) throws XPathExpressionException {
        Indicator sequence;
        sequence = getMultiOrSingleSequence(sequenceNode, name);
        // inspect all elements
        NodeList elements = (NodeList) xpath.evaluate("element", sequenceNode, XPathConstants.NODESET);
        for (int i = 0; i < elements.getLength(); i++) {
            sequence.add(inspectElement(elements.item(i), false));
        }
        // inspect all indicators
        List<Indicator> indicators = inspectIndicators(sequenceNode, originalName, level + 1);
        for (Indicator indicator : indicators) {
            sequence.add(indicator);
        }
        return sequence;
    }

    /**
     * Inspect the element all in the given node.
     * All the elements and indicators are inspected. The argument level tells
     * the method, how deep the recursion in indicators is. This is important
     * when creating names of classes - the name has to be original.
     * @param allNode node of the element 'all'
     * @param originalName the name of the root indicator, in which it is nested
     * @param level deep of the recursion in indicators
     * @return inspected indicator all
     * @throws XPathExpressionException 
     */
    private Indicator inspectAll(Node allNode, String originalName,int level) 
            throws XPathExpressionException {
        Indicator all;
        all = new SingleAll();
        // inspect all elements
        NodeList elements = (NodeList) xpath.evaluate("element", allNode, XPathConstants.NODESET);
        for (int i = 0; i < elements.getLength(); i++) {
            all.add(inspectElement(elements.item(i), false));
        }
        // inspect all indicators
        List<Indicator> indicators = inspectIndicators(allNode, originalName, level + 1);
        for (Indicator indicator : indicators) {
            all.add(indicator);
        }
        return all;
    }

    /**
     * Inspect the element choice in the given node.
     * All the elements and indicators are inspected. The argument level tells
     * the method, how deep the recursion in indicators is. This is important
     * when creating names of multichoices - the name has to be original.
     * @param choiceNode node of the element 'choice'
     * @param originalName the name of the root indicator, in which it is nested
     * @param name actual name in recursion, e.g.RootSubSub1Choice
     * @param level deep of the recursion in indicators
     * @return inspected indicator choice
     * @throws XPathExpressionException 
     */
    private Indicator inspectChoice(Node choiceNode, String originalName,
            String name, int level) throws XPathExpressionException {
        Indicator choice;
        choice = getMultiOrSingleChoice(choiceNode, name);
        // inspect all elements
        NodeList elements = (NodeList) xpath.evaluate("element", choiceNode, XPathConstants.NODESET);
        for (int i = 0; i < elements.getLength(); i++) {
            choice.add(inspectElement(elements.item(i), false));
        }
        // inspect all indicators
        List<Indicator> indicators = inspectIndicators(choiceNode, originalName, level + 1);
        for (Indicator indicator : indicators) {
            choice.add(indicator);
        }
        return choice;
    }

    /**
     * Inspect the element group in the given node.
     * The element has to be inner - has to be only reference to some outer group.
     * The method looks only for the attribute ref and returns indicator of the
     * outer group from the Map groups.
     * @param groupNode node of the element group
     * @return indicator of the refered outer group
     * @throws XPathExpressionException 
     */
    private Indicator inspectGroup(Node groupNode) throws XPathExpressionException {
        String ref = getValueByNameOfAttribute(groupNode, "ref");
        if (ref != null) {
            return groups.get(ref);
        } else {
            throw new IllegalStateException("Wrong structure of XML Schema: "
                    + "element group has no attribute ref. ");
        }
    }
    
    /**
     * Inspect the element simpleContent or complexContent of the given
     * node.
     * In the content can be either extension or restriction. Extension is
     * inspected by separate method and restriction is inspected by inspectSimpleType().
     * @param contentNode node of the element simple or complex Content
     * @param name of the root element, in which it is nested
     * @return new extended complexType
     * @throws XPathExpressionException 
     */
    private ComplexType inspectContent(Node contentNode, String name) throws XPathExpressionException {
        Node extension = (Node) xpath.evaluate("extension", contentNode, XPathConstants.NODE);
        if (extension != null) {
            return inspectExtension(extension, name);
        }
        Node restriction = (Node) xpath.evaluate("restriction", contentNode, XPathConstants.NODE);
        if (restriction != null) {
            ComplexType complexType = new ComplexType(name);
            complexType.setSimpleContent(inspectSimpleType(extension, name));
            return complexType;
        }
        throw new IllegalStateException("Wrong structure of XML Schema: content in "
                + name + " has niether an extension nor a restriction.");
    }

    /**
     * Inspect the element 'extension' of the given node.
     * If the base refers to some complexType, the refered complexType is cloned.
     * Then the method add to the complexType content in extension.
     * @param extensionNode node of the element 'extension'
     * @param name of the root element, in which it is nested
     * @return new extended complexType
     * @throws XPathExpressionException 
     */
    private ComplexType inspectExtension(Node extensionNode, String name) throws XPathExpressionException {
        ComplexType complexType = new ComplexType(name);
        String type = getValueByNameOfAttribute(extensionNode, "base");
        if (type != null) {
            if (isReferenceType(type)) {
                if (complexTypeReferences.contains(type)) {
                    ComplexType base = findComplexTypeInClasses(type);
                    if (base != null) {
                        complexType = base.copy(name);
                    }
                } else {
                    // warning: the complexType is not in references 
                    // -> resolve with simple string type
                    complexType.setSimpleContent(TypeConverter.getDefaultType());
                }
            } else {
                complexType.setSimpleContent(TypeConverter.xmlSchemaToType(type));
            }
        }

        // attributes
        NodeList attributes = (NodeList) xpath.evaluate("attribute", extensionNode, XPathConstants.NODESET);
        for (int i = 0; i < attributes.getLength(); i++) {
            complexType.addAttribute(inspectAttribute(attributes.item(i)));
        }

        // attibute groups
        inspectAttributeGroups(extensionNode, complexType);

        // indicators
        List<Indicator> indicators = inspectIndicators(extensionNode, complexType.getName(), 0);
        if (indicators.size() > 0) {
            if (indicators.get(0) != null) {
                complexType.setIndicator(indicators.get(0));
            }
        }
        return complexType;
    }

    /**
     * Inspect the element attribute of the given node.
     * If the element has the attribute name and type, the type is inspected.
     * The references are not supported for the element 'attribute'.
     * @param attr node of the element 'attribute'
     * @return attribute with the correct type and name
     * @throws XPathExpressionException 
     */
    private Attribute inspectAttribute(Node attr) throws XPathExpressionException {
        String name = getValueByNameOfAttribute(attr, "name");
        if (name != null) {
            String type = getValueByNameOfAttribute(attr, "type");
            if (type != null) {
                if (isReferenceType(type)) {
                    // type is complexType, only write his name, reference is not supported
                    return new Attribute(TypeConverter.getDefaultType(), name);
                }
                // type is simpleType and is written in the attribute 'type'
            }
            // type is simple, but is not written in the attribute 'type'
            return new Attribute((SimpleType) recognizeType(attr, name, false), name);
        } else {
            String ref = getValueByNameOfAttribute(attr, "ref");
            if (ref != null) {
                return new Attribute(TypeConverter.getDefaultType(), ref);
            }
        }
        throw new IllegalStateException("Wrong structure of XML Schema: "
                + "attribute has neither the name nor the ref attribute.");
    }

    /**
     * Inspect the elements attributeGroup of the parent node.
     * The attributeGroup is reference to some outer attributeGroup, so
     * the attribute ref is important. It looks up attributeGroup in the Map
     * attributeGroups and assign all the attributes to the given ComplexType.
     * @param complexTypeNode parent node of the elements 'attributeGroup'
     * @param complexType all the attributes are assigned to it
     * @throws XPathExpressionException 
     */
    private void inspectAttributeGroups(Node complexTypeNode,
            ComplexType complexType) throws XPathExpressionException {
        NodeList attributeGroupNodes = (NodeList) xpath.evaluate("attributeGroup",
                complexTypeNode, XPathConstants.NODESET);
        for (int i = 0; i < attributeGroupNodes.getLength(); i++) {
            String ref = getValueByNameOfAttribute(attributeGroupNodes.item(i), "ref");
            if ((ref != null) && (attributeGroups.containsKey(ref))) {
                for (Attribute attr : attributeGroups.get(ref)) {
                    complexType.addAttribute(attr);
                }
            } else {
                if (ref == null) {
                    throw new IllegalStateException("Wrong structure of XML Schema: "
                            + "no attribute ref in complexType " + complexType.getOriginalName());
                }
            }
        }
    }

    /**
     * Inspect the element simpleType of the given node.
     * Method returns the type of the restriction.
     * If there are enumeration on the restriction, it creates enumerationType 
     * and add the constant to it.
     *
     * @param simpleType - the node of the element 'simpleType'
     * @return type of the restriction or enumerationType
     * @throws XPathExpressionException
     */
    private SimpleType inspectSimpleType(Node simpleType, String name) throws XPathExpressionException {
        Node restriction = (Node) xpath.evaluate("restriction", simpleType, XPathConstants.NODE);
        if (restriction != null) {
            String type = getValueByNameOfAttribute(restriction, "base");
            if (type != null) {
                NodeList enums = (NodeList) xpath.evaluate("enumeration",
                        restriction, XPathConstants.NODESET);
                if (enums.getLength() > 0) {
                    EnumerationType enumerationType = new EnumerationType(name + "Enum");
                    for (int i = 0; i < enums.getLength(); i++) {
                        String value = getValueByNameOfAttribute(enums.item(i), "value");
                        enumerationType.addEnumConstant(value);
                    }
                    addToClasses(enumerationType);
                    return enumerationType;
                } else {
                    return TypeConverter.xmlSchemaToType(type);
                }
            }
        }
        return TypeConverter.getDefaultType();
    }

    /**
     * Method recognizes type firstly by attribute of the element and secondary
     * by inner simpleType. 
     * If the type is an outer ComplexType, returns ComplexTypeReferences. 
     * If the type is simpleType, it is inspected and returned.
     * Otherwise the default type is returned.
     *
     * @param node of the element, its type will be returned
     * @return type
     * @throws XPathExpressionException
     */
    private Type recognizeType(Node node, String name, boolean isRoot) throws XPathExpressionException {
        // inspect attribute type
        String typeName = getValueByNameOfAttribute(node, "type");
        if (typeName != null) {
            if (complexTypeReferences.contains(typeName)) {
                // type refers to an outer complexType
                if (isRoot) {
                    ComplexType complexType = findComplexTypeInClasses(typeName);
                    if (complexType != null) {
                        addToRoots(complexType);
                    }
                }
                return new ComplexTypeReference(typeName);
            } else if (simpleTypes.containsKey(typeName)) {
                return simpleTypes.get(typeName);
            } else {
                return TypeConverter.xmlSchemaToType(typeName);
            }
        } else {
            // inspect simpleType
            Node simpleType = (Node) xpath.evaluate("simpleType", node, XPathConstants.NODE);
            if (simpleType != null) {
                return inspectSimpleType(simpleType, name);
            }
        }
        return TypeConverter.getDefaultType();
    }

    /**
     * The method returns String value of the attribute in the given node.
     * @param node
     * @param name of the attribute
     * @return value of the attribute
     * @throws XPathExpressionException 
     */
    private String getValueByNameOfAttribute(Node node, String name) throws XPathExpressionException {
        Attr attr = (Attr) xpath.evaluate("@" + name, node, XPathConstants.NODE);
        if (attr != null) {
            return attr.getValue();
        }
        return null;
    }

    /**
     * The method decides, if the argument is more than 1.
     * @param maxOccursString
     * @return true if the argument is more than 1
     */
    private boolean moreThanOne(String maxOccursString) {
        Boolean moreThanOne = false;
        if (maxOccursString != null) {
            if ("unbounded".equals(maxOccursString)) {
                moreThanOne = true;
            } else {
                try {
                    int maxOccurs = Integer.parseInt(maxOccursString);
                    if (maxOccurs > 1) {
                        moreThanOne = true;
                    }
                } catch (NumberFormatException ex) {
                }
            }
        }
        return moreThanOne;
    }

    /**
     * The method decides, if the given element can appear only once and 
     * return corresponding class.
     * If the element can appear more than once, the MultiElement is returned.
     * Otherwise SingleElement is returned.
     * @param elementNode node of the element
     * @param type of the element
     * @param name of the element
     * @return multi or single element
     * @throws XPathExpressionException 
     */
    private Element getMultiOrSingleElement(Node elementNode, Type type, String name) throws XPathExpressionException {
        String elementMaxOccurs = getValueByNameOfAttribute(elementNode, "maxOccurs");
        if (moreThanOne(elementMaxOccurs)) {
            return new MultiElement(type, name);
        } else {
            return new SingleElement(type, name);
        }
    }

    /**
     * The method decides, if the given sequence can appear only once and 
     * return corresponding class.
     * If the sequence can appear more than once, the MultiSequence is returned.
     * Otherwise SingleSequence is returned.
     * If it returns MultiSequence, it have to be added in List of classes.
     * @param sequenceNode node of the sequence
     * @param name of the sequence
     * @return multi or single sequence
     * @throws XPathExpressionException 
     */
    private Indicator getMultiOrSingleSequence(Node sequenceNode, String name) throws XPathExpressionException {
        // if the attribute maxOccurs of sequence is more than 1, create a MultiSequence
        String sequenceMaxOccurs = getValueByNameOfAttribute(sequenceNode, "maxOccurs");
        if (moreThanOne(sequenceMaxOccurs)) {
            MultiSequence multiSequence = new MultiSequence(name);
            addToClasses(multiSequence);
            return multiSequence;
        } else {
            return new SingleSequence();
        }
    }

    /**
     * The method decides, if the given choice can appear only once and 
     * return corresponding class.
     * If the choice can appear more than once, the MultiChoice is returned.
     * Otherwise SingleChoice is returned.
     * If it returns MultiChoice, it have to be added in List of classes.
     * @param choiceNode node of the choice
     * @param name of the choice
     * @return multi or single choice
     * @throws XPathExpressionException 
     */
    private Indicator getMultiOrSingleChoice(Node choiceNode, String name) throws XPathExpressionException {
        // if the attribute maxOccurs of choice is more than 1, create a MultiChoice
        String choiceMaxOccurs = getValueByNameOfAttribute(choiceNode, "maxOccurs");
        if (moreThanOne(choiceMaxOccurs)) {
            MultiChoice multiChoice = new MultiChoice(name);
            addToClasses(multiChoice);
            return multiChoice;
        } else {
            return new SingleChoice();
        }
    }

    /**
     * The method safely adds classTemplate in the List of classes.
     * @param classTemplate that will be added
     */
    private void addToClasses(ClassTemplate classTemplate) {
        if (classTemplate != null) {
            classes.add(classTemplate);
        }
    }

    /**
     * The method safely adds complexType in the List of roots.
     * @param complexType that will be added
     */
    private void addToRoots(ComplexType complexType) {
        if (complexType != null) {
            roots.add(complexType);
        }
    }

    /**
     * The method creates name of the indicator by the given level and 
     * count. Each level means one "Sub" in the name.
     * @param name original name, of the root indicator
     * @param indicator type of the indicator
     * @param level deep of the recursion in indicators
     * @param count order of indicator in the same parent indicator
     * @return name of the indicator
     */
    private String createIndicatorName(String name, String indicator, int level, int count) {
        if (level < 0) {
            throw new IllegalArgumentException("level");
        }
        for (int i = 0; i < level; i++) {
            name += "Sub";
        }
        if (count > 0) {
            name += String.valueOf(count);
        }
        name += indicator;
        return name;
    }

    /**
     * The method decides, if the given type is reference or not by
     * the colon in the name.
     * @param type
     * @return true, if the type is reference (has no colon in the name)
     */
    private boolean isReferenceType(String type) {
        if (type == null) {
            throw new IllegalArgumentException("type is null");
        }
        return !(type.contains(":"));
    }

    /**
     * Find a ComplexType by the name in the List of classes.
     * @param name of the ComplexType
     * @return 
     */
    private ComplexType findComplexTypeInClasses(String name) {
        for (ClassTemplate classTemplate : classes) {
            if (classTemplate instanceof ComplexType) {
                ComplexType complexType = (ComplexType) classTemplate;
                if (name.equals(complexType.getOriginalName())) {
                    return complexType;
                }
            }
        }
        return null;
    }
}